#include "Impostor.h"
#include "PortableGL.h"
#include "ModelRenderer.h"
#include <algorithm>
#include "../algebra/Matrix.h"
#include "../geom/GeomMatrix.h"

using namespace glutil;

Impostor::Impostor(const geom::PolyModel & model, int maxTextureSide, float baseScale)
{
	m_baseScale = baseScale;
	geom::FittedBBox box = model.getModelFittedBoundingBox();

	/* The 3 images of the impostor will be rendered to a unique texture in the following way

	    Front  Side  Top	
	   ------------------
	   |xxxxxx|xxxxx|   |       Where X,Y,Z are the width, height and depth of the object BBox, 
	 Y |xxxxxx|xxxxx|----       W=min(X,Z) and V=max(X,Z) when max(X,Z) <= Y 
	   |xxxxxx|xxxxx|xxx| V     else, V = Y, W = min(X,Z) / max(X,Z) * Y
	   ------------------
          X       Z    W
	   */

	float X = box.sideSize(0);
	float Y = box.sideSize(1);
	float Z = box.sideSize(2);

	m_halfSides[0] = X/2;
	m_halfSides[1] = Y/2;
	m_halfSides[2] = Z/2;

	float V = std::max(X,Z);
	float W = std::min(X,Z);
	if(V > Y)
	{
		W = W/V*Y;
		V = Y;
	}
	
	float maxSide = std::max(Y,X+Z+W);
	int maxTexWithMargin = maxTextureSide - 4;
	xToZUV = X/(X+Z+W);
	zToWUV = (X+Z)/(X+Z+W);
	vToUV = V/Y;
	int iX = (int)(X/maxSide * maxTexWithMargin);
	int iZ = (int)(Z/maxSide * maxTexWithMargin);
	int iW = (int)(W/maxSide * maxTexWithMargin);
	int iV = (int)(V/maxSide * maxTextureSide);

	int textureWidth = (int)((X+Z+W)/maxSide * maxTexWithMargin) + 4; // The 4 is for a bit of margin between the images
	int textureHeight = (int)(Y/maxSide * (maxTextureSide));

	xToZUV = iX/float(textureWidth);
	xToZUVPlusMargin = (iX+2) / float(textureWidth);
	zToWUV = (iX+iZ+2)/float(textureWidth);
	zToWUVPlusMargin = (iX+iZ+4)/float(textureWidth);
	vToUV = iV/float(textureHeight);

	FBO image;
	image.init(textureWidth, textureHeight,GL_RGBA8,true);

	ModelRenderer renderer(model);
	glutil::GLInstanceInfo info;
	for(int i=0;i<3;++i)
		info.m_location[i] = 0.0, info.m_rotation[i] = 0.0;
	info.m_scale = 1.0;
	renderer.addInstance(info);

	glActiveTexture(GL_TEXTURE0);
	glDisable(GL_TEXTURE_2D);
	glActiveTexture(GL_TEXTURE1);
	glDisable(GL_TEXTURE_2D);
	glActiveTexture(GL_TEXTURE2);
	glDisable(GL_TEXTURE_2D);

	image.beginCapture();

	glEnable(GL_COLOR_MATERIAL);
	glClearColor(1.0,1.0,1.0,0.0);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	// Front Face
	{
		glViewport(0,0,iX, textureHeight);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glOrtho(box.minCoord(0), box.maxCoord(0), box.minCoord(1), box.maxCoord(1), 0.05,box.sideSize(2) * 3.0);//box.minCoord(2), box.maxCoord(2));
		glMatrixMode(GL_MODELVIEW);

		glLoadIdentity();
		glTranslatef(0,0,-box.sideSize(2));
		renderer.renderAll(true);
	}

	// Side Face
	{
		glViewport(iX+2,0,iZ,textureHeight);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glOrtho(box.minCoord(2), box.maxCoord(2), box.minCoord(1), box.maxCoord(1), 0.05, box.sideSize(0)*3.0);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glTranslatef(0,0,-box.sideSize(0));
		glRotatef(90,0,1,0);

		//glLoadIdentity();
		renderer.renderAll(true);
	}

	// Top Face
	{
		glViewport(textureWidth-iW,0,iW, iV);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glOrtho(box.minCoord(0), box.maxCoord(0), box.minCoord(2), box.maxCoord(2), 0.05, box.sideSize(1)*3.0);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glTranslatef(0,0,-box.sideSize(1));
		glRotatef(-90,1,0,0);
		glRotatef(180,1,0,0);
		renderer.renderAll(true);
	}
	image.endCapture();
	m_image = image.extractTexture();
	glBindTexture(GL_TEXTURE_2D,m_image);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_NEAREST);

	m_shader = new ShaderProgram();
	m_shader->attachShaderFromFile(GL_VERTEX_SHADER,"data/impostor.vert");
	m_shader->attachShaderFromFile(GL_FRAGMENT_SHADER,"data/impostor.frag");
	m_shader->link();
	m_shader->setUniform1i("mainTexture",0);
}

Impostor::Impostor(const Impostor &other)
{
	m_baseScale = other.m_baseScale;
	xToZUV = other.xToZUV;
	xToZUVPlusMargin = other.xToZUVPlusMargin;
	zToWUV = other.zToWUV;
	zToWUVPlusMargin = other.zToWUVPlusMargin;
	vToUV = other.vToUV;
	m_quads = other.m_quads;
	m_image = other.m_image;
	other.m_image = NULL;
	m_halfSides = other.m_halfSides;
	m_shader = other.m_shader;
	other.m_shader = NULL;
}

Impostor::~Impostor()
{
	if(m_image)
		glDeleteTextures(1,&m_image);
	delete m_shader;
}

Quad Impostor::transformQuad(float * quadPoints, const algebra::Matrix4f & matrix) const
{
	Quad result;
	for(int i=0;i<4;++i)
	{	
		algebra::Vector4f v(quadPoints + 4*i);
		algebra::Vector4f transformedV = matrix * v;

		for(int j=0;j<3;++j)
		{
			result.at(i).v[j] = transformedV.at(0,j);
			result.centerPoint.v[j] += transformedV.at(0,j);
		}
	}

	return result;
}

void Impostor::addInstance(const GLInstanceInfo &m)
{
	algebra::Matrix4f translation = geom::createTranslationMatrix(m.m_location[0], m.m_location[1], m.m_location[2]);
	algebra::Matrix4f rotationX= geom::createRotationMatrixAroundX(m.m_rotation[0]/180.0f*M_PI);
	algebra::Matrix4f rotationY= geom::createRotationMatrixAroundY(m.m_rotation[1]/180.0f*M_PI);
	algebra::Matrix4f rotationZ= geom::createRotationMatrixAroundZ(m.m_rotation[2]/180.0f*M_PI);
	algebra::Matrix4f scale = geom::createScalingMatrix(m.m_scale*m_baseScale,m.m_scale*m_baseScale,m.m_scale*m_baseScale);

	algebra::Matrix4f finalMatrix = translation * scale * /*rotationZ **/ rotationY /* *rotationX * */;

	float pointsFrontQuad[4*4] = {
		-m_halfSides[0],m_halfSides[1]*2,0,1, 
		 m_halfSides[0],m_halfSides[1]*2,0,1,
		 m_halfSides[0], 0,0,1,
		-m_halfSides[0], 0,0,1};	

	Quad frontQuad = transformQuad(pointsFrontQuad, finalMatrix) ;
	frontQuad.uvs[0] = geom::Point2D(0,1);
	frontQuad.uvs[1] = geom::Point2D(xToZUV,1);
	frontQuad.uvs[2] = geom::Point2D(xToZUV,0);
	frontQuad.uvs[3] = geom::Point2D(0,0);
	frontQuad.centerPoint = m.m_location;

	m_quads.push_back(frontQuad);

	float pointsSideQuad[4*4] = {
		0,m_halfSides[1]*2,-m_halfSides[2],1,
		0,m_halfSides[1]*2,m_halfSides[2],1,
		0, 0,m_halfSides[2],1,
		0, 0, -m_halfSides[2],1};
	

	Quad sideQuad = transformQuad(pointsSideQuad, finalMatrix);
	sideQuad.uvs[1] = geom::Point2D(zToWUV,1);
	sideQuad.uvs[0] = geom::Point2D(xToZUVPlusMargin,1);
	sideQuad.uvs[3] = geom::Point2D(xToZUVPlusMargin,0);
	sideQuad.uvs[2] = geom::Point2D(zToWUV,0);
	sideQuad.centerPoint = m.m_location;

	m_quads.push_back(sideQuad);

	float pointsTopQuad[4*4] = {
		-m_halfSides[0],m_halfSides[1],m_halfSides[2],1,
		-m_halfSides[0],m_halfSides[1], -m_halfSides[2],1,
		 m_halfSides[0],m_halfSides[1], -m_halfSides[2],1,
		 m_halfSides[0],m_halfSides[1],m_halfSides[2],1};
	
	Quad topQuad = transformQuad(pointsTopQuad, finalMatrix);
	topQuad.uvs[0] = geom::Point2D(zToWUVPlusMargin,0);
	topQuad.uvs[1] = geom::Point2D(zToWUVPlusMargin,vToUV);
	topQuad.uvs[2] = geom::Point2D(1,vToUV);
	topQuad.uvs[3] = geom::Point2D(1,0);
	topQuad.centerPoint = m.m_location;
	m_quads.push_back(topQuad);
}

void Impostor::renderAllFartherThan(const geom::Point3D &camPos, float startDistance)
{
	std::vector<Quad>::const_iterator it;
	std::vector<Quad>::const_iterator end = m_quads.end();

	float startDistSq = startDistance * startDistance;
	glActiveTexture(GL_TEXTURE0);
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D,m_image);

	m_shader->bind();
	glBegin(GL_QUADS);
	for(it = m_quads.begin(); it!=end;++it)
		if(camPos.distanceSquared(it->centerPoint) >= startDistSq)
		{
			glMultiTexCoord2fv(GL_TEXTURE0,it->uvs[0].v);
			glVertex3fv(it->points[0].v);
			glMultiTexCoord2fv(GL_TEXTURE0,it->uvs[1].v);
			glVertex3fv(it->points[1].v);
			glMultiTexCoord2fv(GL_TEXTURE0,it->uvs[2].v);
			glVertex3fv(it->points[2].v);
			glMultiTexCoord2fv(GL_TEXTURE0,it->uvs[3].v);
			glVertex3fv(it->points[3].v);
		}
	glEnd();
	m_shader->unbind();
	glBindTexture(GL_TEXTURE_2D, 0);
    glDisable(GL_TEXTURE_2D);
}


